/* Copyright(C) 2006-2008 Dave Sexton  *
 * http://www.codeplex.com/aip         *
 * http://www.codeplex.com/aip/license *
 ***************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Web.Configuration;
using System.Configuration.Provider;
using DaveSexton.Web.Controls.Configuration;
using System.Web;
using System.IO;
using System.Web.UI;
using System.Configuration;
using DaveSexton.Web.Controls.Resources;

namespace DaveSexton.Web.Controls
{
	/// <summary>
	/// Represents the internal callback method to be invoked by <see cref="AutoInputProtection"/> when an <see cref="AutoInputProtectionImage"/>
	/// is required.
	/// </summary>
	/// <param name="size"><see cref="Size"/> of the image.</param>
	/// <returns><see cref="AutoInputProtectionImage"/> of the specified <paramref name="size"/>.</returns>
	internal delegate AutoInputProtectionImage GenerateAutoInputProtectionImageCallback(Size size);

	/// <summary>
	/// Manages AIP providers, generates <see cref="AutoInputProtectionImage"/> instances and validates their text against user input.
	/// </summary>
	/// <remarks>
	/// <para>
	/// Text, image and filter provider configuration is optional.  If the <c>&lt;autoInputProtection&gt;</c> configuration element is not 
	/// specified then AIP will use default text and image providers, although no filter providers are used by default.
	/// </para>
	/// <include file='comments.xml' path='//para[@id="DefaultTextProvider_Rules"]'/>
	/// <include file='comments.xml' path='//para[@id="DefaultImageProvider_Rules"]'/>
	/// <para>
	/// The <see cref="UserMode"/> property indicates how users are identified and the <see cref="PersistenceMode"/> property indicates
	/// how challenges are stored on the server.
	/// <include file='comments.xml' path='//text[@id="PersistenceMode.Session-Reccomendation"]/child::node()'/>
	/// See the example in this help topic.
	/// </para>
	/// <!--warn about indefinite persistence:--><include file='comments.xml' path='//note[@id="ValidationKeepAlive"]'/>
	/// </remarks>
	/// <include file='examples.xml' path='//example[@id="configSections"]'/>
	/// <include file='examples.xml' path='//example[@id="configSession"]'/>
	/// <seealso cref="AutoInputProtectionControl"/>
	/// <seealso cref="Configuration.AutoInputProtectionSection"/>
	/// <threadsafety static="true" />
	public static class AutoInputProtection
	{
		#region Public Properties
		/// <summary>
		/// Gets or sets the virtual path to the AIP image request handler.  The default value is <em>~/AIP.ashx</em>.
		/// </summary>
		/// <remarks>
		/// The value of this property must be configured in the configuration file using the <c>httpHandlers</c> section.
		/// </remarks>
		/// <example>
		/// <para>
		/// The following example shows the correct way to register the AIP image handler in a configuration file.
		/// </para>
		/// <include file='examples.xml' path='//example[@id="handler"]/*'/>
		/// </example>
		public static string ImageRequestHandlerPath
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				return imageRequestHandlerPath ?? "~/AIP.ashx";
			}
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			set
			{
				imageRequestHandlerPath = value;
			}
		}

		/// <summary>
		/// Gets or sets how AIP will identify users.
		/// </summary>
		/// <remarks>
		///	<include file='comments.xml' path='//para[@id="UserMode_Benefits"]'/>
		///	<para>
		///	Refer to the individual <see cref="AutoInputProtectionUserMode"/> options for more information.
		///	</para>
		///	<para>
		///	<see cref="UserMode"/> is typically set to <see cref="AutoInputProtectionUserMode.None">None</see> when 
		///	<see cref="PersistenceMode"/> is set to <see cref="AutoInputProtectionPersistenceMode.Session">Session</see>
		///	since requests will automatically be identified as belonging to the user that owns the current session.
		///	</para>
		///	<!--warn about indefinite persistence:--><include file='comments.xml' path='//note[@id="ValidationKeepAlive"]'/>
		/// </remarks>
		/// <include file='examples.xml' path='//example[@id="configSession"]'/>
		/// <seealso cref="AutoInputProtectionUserMode"/>
		/// <seealso cref="Configuration.AutoInputProtectionSection.UserMode"/>
		/// <exception cref="System.ComponentModel.InvalidEnumArgumentException">The specified value is not an 
		/// <see cref="AutoInputProtectionUserMode"/> constant.</exception>
		public static AutoInputProtectionUserMode UserMode
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				EnsureProviders();

				return userMode;
			}
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			set
			{
				if (!Enum.IsDefined(typeof(AutoInputProtectionUserMode), value))
					throw new System.ComponentModel.InvalidEnumArgumentException("value", (int) value, typeof(AutoInputProtectionUserMode));

				userMode = value;
			}
		}

		/// <summary>
		/// Gets or sets where AIP will persist challenges on the server.
		/// </summary>
		/// <remarks>
		/// <para>
		/// <include file='comments.xml' path='//text[@id="PersistenceMode.Session-Reccomendation"]/child::node()'/>
		/// See the example in <see cref="AutoInputProtection"/>.
		/// </para>
		/// <!--warn about indefinite persistence:--><include file='comments.xml' path='//note[@id="ValidationKeepAlive"]'/>
		/// </remarks>
		/// <h4>Note to developers:</h4>
		/// <para>
		/// Settings this property will assign the <see cref="getPersistedValue"/> delegate to an appropriate method
		/// that can be used internally by the members of this class to retrieve and remove values from various storage 
		/// systems without having to read this property.
		/// </para>
		/// <include file='examples.xml' path='//example[@id="configSession"]'/>
		/// <seealso cref="GetPersistedValueStrategy"/>
		/// <seealso cref="AutoInputProtectionPersistenceMode"/>
		/// <seealso cref="Configuration.AutoInputProtectionSection.PersistenceMode"/>
		/// <exception cref="System.ComponentModel.InvalidEnumArgumentException">The specified value is not an 
		/// <see cref="AutoInputProtectionPersistenceMode"/> constant.</exception>
		public static AutoInputProtectionPersistenceMode PersistenceMode
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				EnsureProviders();

				return persistenceMode;
			}
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			set
			{
				if (!Enum.IsDefined(typeof(AutoInputProtectionPersistenceMode), value))
					throw new System.ComponentModel.InvalidEnumArgumentException("value", (int) value, typeof(AutoInputProtectionPersistenceMode));

				persistenceMode = value;

				switch (value)
				{
					case AutoInputProtectionPersistenceMode.Cache:
						getPersistedValue = GetCacheValue;
						break;
					case AutoInputProtectionPersistenceMode.Session:
						getPersistedValue = GetSessionValue;
						break;
				}
			}
		}

		/// <summary>
		/// Gets the default <see cref="AutoInputProtectionTextProvider"/>.
		/// </summary>
		/// <seealso cref="Configuration.AutoInputProtectionSection.DefaultTextProvider"/>
		public static AutoInputProtectionTextProvider DefaultTextProvider
		{
			get
			{
				EnsureProviders();

				return textProvider;
			}
		}

		/// <summary>
		/// Gets a collection of <see cref="AutoInputProtectionTextProvider"/> instances from the types registered 
		/// in the configuration file.
		/// </summary>
		/// <seealso cref="Configuration.AutoInputProtectionSection.TextProviders"/>
		public static AutoInputProtectionTextProviderCollection TextProviders
		{
			get
			{
				EnsureProviders();

				return textProviders;
			}
		}

		/// <summary>
		/// Gets the default <see cref="AutoInputProtectionImageProvider"/>.
		/// </summary>
		/// <seealso cref="Configuration.AutoInputProtectionSection.DefaultImageProvider"/>
		public static AutoInputProtectionImageProvider DefaultImageProvider
		{
			get
			{
				EnsureProviders();

				return imageProvider;
			}
		}

		/// <summary>
		/// Gets a collection of <see cref="AutoInputProtectionImageProvider"/> instances from the types registered 
		/// in the configuration file.
		/// </summary>
		/// <seealso cref="Configuration.AutoInputProtectionSection.ImageProviders"/>
		public static AutoInputProtectionImageProviderCollection ImageProviders
		{
			get
			{
				EnsureProviders();

				return imageProviders;
			}
		}

		/// <summary>
		/// Gets a collection of <see cref="AutoInputProtectionFilterProvider"/> instances from the types registered 
		/// in the configuration file.
		/// </summary>
		/// <seealso cref="Configuration.AutoInputProtectionSection.FilterProviders"/>
		public static AutoInputProtectionFilterProviderCollection FilterProviders
		{
			get
			{
				EnsureProviders();

				return filterProviders;
			}
		}
		#endregion

		#region Private / Protected
		/// <summary>
		/// Represents the method that gets the image or text identified by the specified <paramref name="internalKey"/> for the current 
		/// <paramref name="context"/> from persisted storage on the server.
		/// </summary>
		/// <remarks>
		/// The strategy used by AIP is chosen when the <see cref="PersistenceMode"/> property is set.  A value of <see cref="AutoInputProtectionPersistenceMode.Cache"/>
		/// will cause the <see cref="GetCacheValue"/> method to be used and a value of <see cref="AutoInputProtectionPersistenceMode.Session"/> will cause the 
		/// <see cref="GetSessionValue"/> method to be used.
		/// </remarks>
		/// <seealso cref="PersistenceMode"/>
		/// <seealso cref="GetCacheValue"/>
		/// <seealso cref="GetSessionValue"/>
		/// <seealso cref="getPersistedValue"/>
		/// <param name="context"><see cref="HttpContext"/> for the current request.</param>
		/// <param name="internalKey">An internal key that identifies the image or text in server storage.</param>
		/// <param name="remove">Specifies whether the value should be removed from the server.</param>
		/// <returns>The persisted value from server storage.</returns>
		private delegate object GetPersistedValueStrategy(HttpContext context, string internalKey, bool remove);

		private static readonly AutoInputProtectionTextProviderCollection textProviders = new AutoInputProtectionTextProviderCollection();
		private static readonly AutoInputProtectionImageProviderCollection imageProviders = new AutoInputProtectionImageProviderCollection();
		private static readonly AutoInputProtectionFilterProviderCollection filterProviders = new AutoInputProtectionFilterProviderCollection();
		private static AutoInputProtectionImageProvider imageProvider;
		private static AutoInputProtectionTextProvider textProvider;
		private static AutoInputProtectionUserMode userMode;
		private static AutoInputProtectionPersistenceMode persistenceMode;
		/// <summary>
		/// Represents the <see cref="GetCacheValue"/> or <see cref="GetSessionValue"/> method, depending upon the value of the 
		/// <see cref="PersistenceMode"/> property.
		/// </summary>
		/// <seealso cref="GetPersistedValueStrategy"/>
		private static GetPersistedValueStrategy getPersistedValue;
		private static string imageRequestHandlerPath;
		private volatile static bool initialized;
		private static bool initializedTextProviders, initializedImageProviders;
		#endregion

		#region Methods
		/// <summary>
		/// Ensures that the configuration file has been read and that all state has been parsed and loaded.
		/// </summary>
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		private static void EnsureProviders()
		{
			if (!initialized)
			{
				AutoInputProtectionSection section = (AutoInputProtectionSection) WebConfigurationManager.GetSection("autoInputProtection");
				bool hasSection = section != null;

				if (!hasSection)
					// Create default providers below;  section will not be read-only because it isn't being loaded from a configuration file.
					section = new AutoInputProtectionSection();

				userMode = section.UserMode;
				PersistenceMode = section.PersistenceMode;		// set the property, not its field

				InitializeTextProviders(hasSection, section);
				InitializeImageProviders(hasSection, section);

				ProvidersHelper.InstantiateProviders(section.FilterProviders, filterProviders, typeof(AutoInputProtectionFilterProvider));

				// NOTE: if initialization fails then this variable should remain false, so don't include it in a finally block
				initialized = true;
			}
		}

		/// <summary>
		/// Initializes the <see cref="TextProviders"/> collection from the specified <see cref="AutoInputProtectionSection"/> configuration <paramref name="section"/>.
		/// </summary>
		/// <remarks>
		/// <include file='comments.xml' path='//para[@id="DefaultTextProvider_Rules"]'/>
		/// </remarks>
		/// <exception cref="InvalidOperationException">The <em>defaultTextProvider</em> attribute is specified (as any value other than the default value) and a provider by that name 
		/// is not specified in the <em>textProviders</em> collection element.</exception>
		/// <param name="configHasSection">Indicates whether the specified <paramref name="section"/> exists in the configuration file or if a default instance has been created.</param>
		/// <param name="section"><see cref="AutoInputProtectionSection"/> configuration section that specifies all of the available <see cref="AutoInputProtectionTextProvider"/> 
		/// implementations to be instantiated.</param>
		private static void InitializeTextProviders(bool configHasSection, AutoInputProtectionSection section)
		{
			if (configHasSection)
			{
				if (!initializedTextProviders)
				{
					ProvidersHelper.InstantiateProviders(section.TextProviders, textProviders, typeof(AutoInputProtectionTextProvider));
					initializedTextProviders = true;
				}

				if (section.DefaultTextProvider.Equals(section.ElementInformation.Properties["defaultTextProvider"].DefaultValue))
				{
					if (textProviders.Count > 0)
						textProvider = textProviders[0];
					else
						textProvider = new BasicEnglishAutoInputProtectionTextProvider(
							new[] { Color.Black, Color.Red, Color.Brown },
							new[] { new Font("Times New Roman", 1), new Font("Arial", 1), new Font("Microsoft Sans Serif", 1) });
				}
				else
					textProvider = textProviders[section.DefaultTextProvider];

				if (textProvider == null)
					throw new InvalidOperationException(Errors.Config_Missing_TextProvider);
			}
			else if (textProvider == null)
			{
				section.TextProviders.Add(CreateProviderSettings(section.DefaultTextProvider, typeof(BasicEnglishAutoInputProtectionTextProvider),
					new KeyValuePair<string, string>("colors", "Black, Red, Brown"),
					new KeyValuePair<string, string>("fonts", "Times New Roman, Arial, Microsoft Sans Serif")));

				ProvidersHelper.InstantiateProviders(section.TextProviders, textProviders, typeof(AutoInputProtectionTextProvider));
				textProvider = textProviders[section.DefaultTextProvider];
			}
		}

		/// <summary>
		/// Initializes the <see cref="ImageProviders"/> collection from the specified <see cref="AutoInputProtectionSection"/> configuration <paramref name="section"/>.
		/// </summary>
		/// <remarks>
		/// <include file='comments.xml' path='//para[@id="DefaultImageProvider_Rules"]'/>
		/// </remarks>
		/// <exception cref="InvalidOperationException">The <em>defaultImageProvider</em> attribute is specified (as any value other than the default value) and a provider by that name 
		/// is not specified in the <em>imageProviders</em> collection element.</exception>
		/// <param name="configHasSection">Indicates whether the specified <paramref name="section"/> exists in the configuration file or if a default instance has been created.</param>
		/// <param name="section"><see cref="AutoInputProtectionSection"/> configuration section that specifies all of the available <see cref="AutoInputProtectionImageProvider"/> 
		/// implementations to be instantiated.</param>
		private static void InitializeImageProviders(bool configHasSection, AutoInputProtectionSection section)
		{
			if (configHasSection)
			{
				if (!initializedImageProviders)
				{
					ProvidersHelper.InstantiateProviders(section.ImageProviders, imageProviders, typeof(AutoInputProtectionImageProvider));
					initializedImageProviders = true;
				}

				if (section.DefaultImageProvider.Equals(section.ElementInformation.Properties["defaultImageProvider"].DefaultValue))
				{
					if (imageProviders.Count > 0)
						imageProvider = imageProviders[0];
					else
						imageProvider = new LineNoiseAutoInputProtectionImageProvider();
				}
				else
					imageProvider = imageProviders[section.DefaultImageProvider];

				if (imageProvider == null)
					throw new InvalidOperationException(Errors.Config_Missing_ImageProvider);
			}
			else if (imageProvider == null)
			{
				section.ImageProviders.Add(CreateProviderSettings(section.DefaultImageProvider, typeof(LineNoiseAutoInputProtectionImageProvider)));

				ProvidersHelper.InstantiateProviders(section.ImageProviders, imageProviders, typeof(AutoInputProtectionImageProvider));
				imageProvider = imageProviders[section.DefaultImageProvider];
			}
		}

		/// <summary>
		/// Creates a new <see cref="ProviderSettings"/> instance for the specified provider <paramref name="name"/> and
		/// <paramref name="type"/>, with the specified <paramref name="parameters"/>.
		/// </summary>
		/// <param name="name">Name of the provider.</param>
		/// <param name="type"><see cref="Type"/> of the provider.</param>
		/// <param name="parameters"><see cref="String"/> array of parameters to add to the settings.</param>
		/// <returns><see cref="ProviderSettings"/> instance for a provider.</returns>
		private static ProviderSettings CreateProviderSettings(string name, Type type, params KeyValuePair<string, string>[] parameters)
		{
			ProviderSettings settings = new ProviderSettings(name, type.AssemblyQualifiedName);

			foreach (KeyValuePair<string, string> parameter in parameters)
				settings.Parameters.Add(parameter.Key, parameter.Value);

			return settings;
		}

		/// <summary>
		/// Compares the specified <paramref name="text"/> and <paramref name="imageText"/> arguments for equality using 
		/// <see cref="DefaultTextProvider"/>.
		/// </summary>
		/// <seealso cref="AutoInputProtectionControl.Validate(string,string)"/>
		/// <param name="text">The text entered by the user.</param>
		/// <param name="imageText">The text that appears on an image generated by <see cref="DefaultImageProvider"/>.</param>
		/// <returns><see langword="true">True</see> if the strings are equal; otherwise, <see langword="false"/>.</returns>
		/// <seealso cref="AutoInputProtectionTextProvider"/>
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static bool ValidateUserInput(string text, string imageText)
		{
#if DEBUG
			bool passed = DefaultTextProvider.ValidateUserInput(text, imageText);

			HttpContext.Current.Trace.Write("AIP Validation", "Answer: " + imageText + "; User input: " + text + "; " + ((passed) ? "passed" : "failed"));
			System.Diagnostics.Debug.WriteLine("Answer: " + imageText + "; User input: " + text + "; " + ((passed) ? "passed" : "failed"), "AIP Validation");

			return passed;
#else
			return DefaultTextProvider.ValidateUserInput(text, imageText);
#endif
		}

		/// <summary>
		/// Creates a random, in-memory <see cref="AutoInputProtectionImage" /> that is of the specified <paramref name="size" />.
		/// </summary>
		/// <remarks>
		/// The image is generated using <see cref="DefaultTextProvider" />, <see cref="DefaultImageProvider" /> and all of the configured <see cref="FilterProviders"/>.
		/// </remarks>
		/// <seealso cref="GenerateAutoInputProtectionImage"/>
		/// <seealso cref="AutoInputProtectionTextProvider"/>
		/// <seealso cref="AutoInputProtectionImageProvider"/>
		/// <seealso cref="AutoInputProtectionFilterProvider"/>
		/// <exception cref="ArgumentOutOfRangeException">The specified <paramref name="size"/> is less than 1 x 1.</exception>
		/// <param name="size">Width and height of the <see cref="AutoInputProtectionImage" />, in pixels.</param>
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static AutoInputProtectionImage GenerateRandomAutoInputProtectionImage(Size size)
		{
			if (size.Width < 1 || size.Height < 1)
				throw new ArgumentOutOfRangeException("size", size, Errors.SizePositiveIntRequired);

			EnsureProviders();

			AutoInputProtectionImage image = imageProvider.GenerateRandomAutoInputProtectionImage(size, textProvider);
			image.filters = filterProviders;

			return image;
		}

		/// <summary>
		/// Creates an in-memory <see cref="AutoInputProtectionImage" /> that is of the specified <paramref name="size" />.
		/// </summary>
		/// <remarks>
		/// It is up to the <see cref="AutoInputProtectionImageProvider"/> implementation to make use of the <paramref name="name"/> argument, although
		/// typically this argument will be ignored and the result will be the same as calling <see cref="GenerateRandomAutoInputProtectionImage"/>.
		/// <para>
		/// For example, <see cref="ResourceAutoInputProtectionImageProvider"/> uses the <paramref name="name"/> argument to explicitly identify 
		/// an embedded resource.
		/// </para>
		/// </remarks>
		/// <seealso cref="GenerateRandomAutoInputProtectionImage"/>
		/// <seealso cref="AutoInputProtectionTextProvider"/>
		/// <seealso cref="AutoInputProtectionImageProvider"/>
		/// <seealso cref="AutoInputProtectionFilterProvider"/>
		/// <exception cref="ArgumentOutOfRangeException">The specified <paramref name="size"/> is less than 1 x 1.</exception>
		/// <param name="name"><see cref="String"/> that identifies a particular named resource, style or image to be used by the image provider.</param>
		/// <param name="size">Width and height of the <see cref="AutoInputProtectionImage" />, in pixels.</param>
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static AutoInputProtectionImage GenerateAutoInputProtectionImage(string name, Size size)
		{
			if (size.Width < 1 || size.Height < 1)
				throw new ArgumentOutOfRangeException("size", size, Errors.SizePositiveIntRequired);

			EnsureProviders();

			AutoInputProtectionImage image = imageProvider.GenerateAutoInputProtectionImage(name, size, textProvider);
			image.filters = filterProviders;

			return image;
		}

		/// <summary>
		/// Calculates a URI for an AIP image based on the specified request <paramref name="context"/> and <paramref name="publicKey"/>.
		/// </summary>
		/// <remarks>
		/// When using cookieless sessions all requests must contain the session ID.  This method automatically ensures that the URL it creates
		/// contains the session ID when cookieless sessions are enabled and <see cref="UserMode"/> is set to 
		/// <see cref="AutoInputProtectionUserMode.SessionOrClient"/>.
		/// </remarks>
		/// <param name="context"><see cref="HttpContext"/> for the current request.</param>
		/// <param name="publicKey"><see cref="String"/> that a client uses to identify an AIP test on the server.</param>
		/// <returns>A unique URI that identifies an AIP image on the server.</returns>
		internal static string GetImageUrl(HttpContext context, string publicKey)
		{
			string url = string.Format(System.Globalization.CultureInfo.InvariantCulture,
				"{0}?k={1}", VirtualPathUtility.ToAbsolute(ImageRequestHandlerPath), HttpUtility.UrlEncode(publicKey));

			if (UserMode == AutoInputProtectionUserMode.SessionOrClient && context.Session != null && context.Session.IsCookieless)
				return context.Response.ApplyAppPathModifier(url);
			else
				return url;
		}

		/// <summary>
		/// Caches the specified AIP <paramref name="text"/> for the specified request <paramref name="context"/> and <paramref name="publicKey"/>, 
		/// with the specified timeout settings.
		/// </summary>
		/// <remarks>
		/// <para>
		/// The specified <paramref name="text"/> belongs to an AIP image that is generated by calling <see cref="GenerateAndCacheImage"/>.
		/// The <see cref="AutoInputProtectionControl"/> calls this method after the image is generated, and therefore after the 
		/// <paramref name="text"/> has been generated as well.
		/// </para>
		/// <para>
		/// Note that the control only calls this method after validation has occurred to ensure that a new test will not replace an existing test 
		/// that has not yet been validated.
		/// </para>
		/// <para>
		/// The <paramref name="validationKeepAlive"/> and <paramref name="validationTimeoutSeconds"/> arguments are ignored unless <see cref="PersistenceMode"/>
		/// is set to <see cref="AutoInputProtectionPersistenceMode.Cache"/> (the default value).  The alternative value, 
		/// <see cref="AutoInputProtectionPersistenceMode.Session"/>, will cause this method to store the <paramref name="text"/> in the ASP.NET session, 
		/// which does not provide an automatic timeout facility.  The benefit of storing the <paramref name="text"/> in the session is that it enables
		/// web farm scenarios when session state is stored out-of-process, such as in a database.
		/// </para>
		/// <para>
		/// If <paramref name="validationKeepAlive"/> is <see langword="false"/> then the test will no longer be valid when the number of seconds specified 
		/// by <paramref name="validationTimeoutSeconds"/> has elapsed.  Validation requests will subsequently fail until a new test is requested, which usually 
		/// occurs simultaneously.
		/// </para>
		/// </remarks>
		/// <seealso cref="GetCacheKeyForUser"/>
		/// <seealso cref="GenerateAndCacheImage"/>
		/// <param name="context"><see cref="HttpContext"/> for the current request.</param>
		/// <param name="text">AIP text that corresponds to an AIP image identified by the specified <paramref name="publicKey"/>.</param>
		/// <param name="publicKey"><see cref="String"/> that a client uses to identify an AIP image on the server.</param>
		/// <param name="validationKeepAlive"><see langword="True"/> to indicate that the text will never expire; otherwise, <see langword="false"/> 
		/// to indicate that the text should expire the number of seconds specified by <paramref name="validationTimeoutSeconds"/>, starting from when
		/// method is invoked.</param>
		/// <param name="validationTimeoutSeconds">Number of seconds to keep the text cached on the server when <paramref name="validationKeepAlive"/> is 
		/// <see langword="false"/>.</param>
		internal static void CacheText(HttpContext context, string text, string publicKey,
			bool validationKeepAlive, int validationTimeoutSeconds)
		{
			string textKey = GetCacheKeyForUser(context) ?? "t_" + publicKey;

			if (PersistenceMode == AutoInputProtectionPersistenceMode.Cache)
			{
				context.Cache.Insert(textKey, text, null,
					(validationKeepAlive) ? System.Web.Caching.Cache.NoAbsoluteExpiration : DateTime.Now.AddSeconds(validationTimeoutSeconds),
					System.Web.Caching.Cache.NoSlidingExpiration,
					System.Web.Caching.CacheItemPriority.NotRemovable, null);
			}
			else
				context.Session[textKey] = text;

#if DEBUG
			context.Trace.Write("AIP Answer Cached", "Answer: " + text);
			System.Diagnostics.Debug.WriteLine(text + "; Key: " + textKey, "AIP Answer Cached");
#endif
		}

		/// <summary>
		/// Invokes the specified <paramref name="callback"/> to generate an <see cref="AutoInputProtectionImage"/> with the specified 
		/// <paramref name="size"/>, for the specified request <paramref name="context"/> and with the specified timeout settings.
		/// </summary>
		/// <remarks>
		/// <para>
		/// Note that this method does not persist the image's text on the server.  <see cref="AutoInputProtectionControl"/> calls 
		/// <see cref="CacheText"/> after validation has occurred to ensure that a new test does not replace an existing test that 
		/// has not yet been validated.
		/// </para>
		/// </remarks>
		/// <seealso cref="GetCacheKeyForUser"/>
		/// <seealso cref="CacheText"/>
		/// <exception cref="InvalidOperationException">The specified <paramref name="callback"/> returned <see langword="null"/>, 
		/// <see cref="AutoInputProtectionImage.Text"/> is null or empty, or <see cref="PersistenceMode"/> is set to 
		/// <see cref="AutoInputProtectionPersistenceMode.Session"/> but sessions are not enabled for the application.</exception>
		/// <param name="context">The <see cref="HttpContext"/> for which the method is being invoked.</param>
		/// <param name="size">The size of the <see cref="AutoInputProtectionImage"/> that the specified <paramref name="callback"/> should generate.</param>
		/// <param name="requestKeepAlive"><see langword="true">True</see> to use <paramref name="requestTimeoutSeconds"/> as a sliding expiration or 
		/// <see langword="false"/> to use it as an absolute expiration.</param>
		/// <param name="requestTimeoutSeconds">The number of seconds to keep the image cached on the server, either from the time that this method is 
		/// invoked or from the last time the image was requested, depending upon the value of <paramref name="requestKeepAlive"/>.</param>
		/// <param name="callback">A method with the <see cref="GenerateAutoInputProtectionImageCallback"/> signature that will be invoked with the 
		/// specified <paramref name="size"/> to generate an <see cref="AutoInputProtectionImage"/> for the current request.</param>
		/// <param name="publicKey">String that can be used by the client to request the image from the server.</param>
		/// <returns>An <see cref="AutoInputProtectionImage"/> generated by the specified <paramref name="callback"/>.</returns>
		internal static AutoInputProtectionImage GenerateAndCacheImage(HttpContext context, Size size,
			bool requestKeepAlive, int requestTimeoutSeconds, GenerateAutoInputProtectionImageCallback callback, out string publicKey)
		{
			MemoryStream stream;
			AutoInputProtectionImage aipImage;
			int attempt = 0;
			string text;

			do
			{
				aipImage = callback(size);

				if (aipImage == null)
					throw new InvalidOperationException(Errors.AIP_GenerateImageCallbackReturnedNull);

				text = aipImage.Text;

				if (string.IsNullOrEmpty(text))
					throw new InvalidOperationException(Errors.AIP_Text_NullOrEmpty);

				publicKey = GeneratePublicCacheKey(context, text);
			}
			while (!CreateImageStream(ref attempt, context, aipImage, out stream));

			string imageKey = GetCacheKeyForImage(context, publicKey);

			if (PersistenceMode == AutoInputProtectionPersistenceMode.Cache)
			{
				context.Cache.Insert(imageKey, new Pair(requestKeepAlive, stream), null,
					(requestKeepAlive) ? System.Web.Caching.Cache.NoAbsoluteExpiration : DateTime.Now.AddSeconds(requestTimeoutSeconds),
					(requestKeepAlive) ? TimeSpan.FromSeconds(requestTimeoutSeconds) : System.Web.Caching.Cache.NoSlidingExpiration,
					System.Web.Caching.CacheItemPriority.NotRemovable, null);
			}
			else if (context.Session == null)
				throw new InvalidOperationException(Resources.Errors.SessionStateRequired);
			else
				context.Session[imageKey] = new Pair(requestKeepAlive, stream);

#if DEBUG
			context.Trace.Write("AIP Image Generated", "Answer: " + text + "; Size: " + size.ToString() + "; Public key: " + publicKey);
			System.Diagnostics.Debug.WriteLine("Answer: " + text + "; Size: " + size.ToString() + "; Key: " + imageKey, "AIP Image Generated");
#endif

			return aipImage;
		}

		/// <summary>
		/// Generates the specified <paramref name="aipImage"/> and loads it into a <see cref="MemoryStream"/> that is assigned to the 
		/// <paramref name="stream"/> output argument.
		/// </summary>
		/// <remarks>
		/// The specified <paramref name="attempt"/> argument, passed by reference, is incremented by 1 if the operation fails while the 
		/// image is being loaded into a <see cref="MemoryStream"/>.  If the third attempt fails then <see cref="ArgumentException"/> is thrown.
		/// <para>
		/// It is unknown why <see cref="ArgumentException"/> occurs but it seems to be random and doesn't happen very often.  It also appears
		/// to have something to do with <see cref="Image.Save(Stream,System.Drawing.Imaging.ImageFormat)"/> corrupting the image since the first call to <see cref="Image.Width"/> always 
		/// succeeds but the second one sometime fails; however, sometimes the exception occurs on the call to <see cref="Image.Save(Stream,System.Drawing.Imaging.ImageFormat)"/> itself.
		/// </para>
		/// <para>
		/// Since this problem is rare, simply trying the operation again has always worked during testing, on the first retry.  However, no more 
		/// than 3 attempts are permitted anyway.
		/// </para>
		/// </remarks>
		/// <exception cref="ArgumentException">An error occurred while wrapping the image in a <see cref="MemoryStream"/>.  See the remarks section for more infomration.</exception>
		/// <param name="attempt">A reference to the current attempt number, starting at zero.</param>
		/// <param name="context">The <see cref="HttpContext"/> for which the image will be generated.</param>
		/// <param name="aipImage">The <see cref="AutoInputProtectionImage"/> to be generated.</param>
		/// <param name="stream">An output argument that is a <see cref="MemoryStream"/> containing the image generated by the specified <paramref name="aipImage"/> argument.</param>
		/// <returns><see langword="true">True</see> if the output <paramref name="stream"/> has been created successfully; otherwise, <see langword="false"/> when the operation has 
		/// failed with less than 3 attempts.</returns>
		private static bool CreateImageStream(ref int attempt, HttpContext context, AutoInputProtectionImage aipImage, out MemoryStream stream)
		{
			stream = null;

			using (Image image = aipImage.CreateCompositeImage())
			{
				try
				{
					stream = new MemoryStream(image.Width * image.Height);

					image.Save(stream, System.Drawing.Imaging.ImageFormat.Jpeg);
				}
				catch (ArgumentException ex)
				/*
				 * [ArgumentException: Parameter is not valid.]
				 *		at System.Drawing.Image.get_Width()
				 *									- or -
				 *		at System.Drawing.Image.Save(Stream stream, ImageCodecInfo encoder, EncoderParameters encoderParams)
				 *		
				 * See the remarks documentation comment.
				 */
				{
					if (stream != null)
						stream.Dispose();

					attempt++;

					if (attempt == 3)
						// An image cannot be generated, probably due to a provider error.
						// The image request handler will return an auto-generated error message if this exception is caught and the AIP control is allowed to render.
						throw;

#if DEBUG
					context.Trace.Write("AIP Image Persistence Error #" + attempt, ex.ToString());
					System.Diagnostics.Debug.WriteLine(ex.ToString(), "AIP Image Persistence Error #" + attempt);
#endif
					return false;
				}
			}

			return true;
		}

		/// <summary>
		/// Generates a string, for the specified <paramref name="context"/> and based on the specified <paramref name="text"/>, that can be used by a client to 
		/// request an image from the server.
		/// </summary>
		/// <seealso cref="GetCacheKeyForUser"/>
		/// <seealso cref="GetCacheKeyForImage(string,string)"/>
		/// <param name="context">The <see cref="HttpContext"/> in which the string is being generated.</param>
		/// <param name="text">The text that appears on the image to which the generated string applies.</param>
		/// <returns>Base64-encoded <strong>SHA1</strong> hash of a string that consists of the specified <paramref name="text"/>, the current request 
		/// path and <see cref="Environment.TickCount"/>.</returns>
		private static string GeneratePublicCacheKey(HttpContext context, string text)
		{
			using (System.Security.Cryptography.SHA1Managed sha1 = new System.Security.Cryptography.SHA1Managed())
			{
				// Hashed bytes: 
				// - text is pseudo-random and may clash with other requests if it's used alone;
				//   however, the text will not be known to an automated process that makes the 
				//   request, unlike the other values that are used.
				// - Path is used so that different request paths should generate different output
				// - The current tick count is used to add some more variation between post-backs

				// NOTE: Since I'm using the SHA1 algorithm anyway some of these points may be moot.

				return Convert.ToBase64String(sha1.ComputeHash(
					Encoding.UTF8.GetBytes(text + context.Request.Path + Environment.TickCount)),
					Base64FormattingOptions.None);
			}
		}

		/// <summary>
		/// Generates a string that uniquely identifies the user of the specified request <paramref name="context"/>.
		/// </summary>
		/// <remarks>
		/// The key generated depends upon the value of the <see cref="UserMode"/> property.  A value of 
		/// <see cref="AutoInputProtectionUserMode.None"/> will produce <see langword="null"/>, a value of 
		/// <see cref="AutoInputProtectionUserMode.Client"/> will produce the exact user host address, or if it's 
		/// unavailable then the user host name, and a value of <see cref="AutoInputProtectionUserMode.SessionOrClient"/>
		/// will produce the current <see cref="System.Web.SessionState.HttpSessionState.SessionID"/> if sessions are enabled or the same 
		/// result as for <see cref="AutoInputProtectionUserMode.Client"/> is sessions are not enabled.
		/// </remarks>
		/// <seealso cref="GeneratePublicCacheKey"/>
		/// <seealso cref="GetCacheKeyForImage(string,string)"/>
		/// <param name="context">The <see cref="HttpContext"/> in which the key is generated.</param>
		/// <returns>String that uniquely identifies the user of the specified <paramref name="context"/>.</returns>
		private static string GetCacheKeyForUser(HttpContext context)
		{
			AutoInputProtectionUserMode mode = UserMode;

			if (mode == AutoInputProtectionUserMode.None)
				return null;
			if (mode == AutoInputProtectionUserMode.Client || (mode == AutoInputProtectionUserMode.SessionOrClient && context.Session == null))
				return context.Request.UserHostAddress ?? context.Request.UserHostName;
			else
				return context.Session.SessionID;
		}

		/// <summary>
		/// Generates a string that uniquely identifies an image being persisted on the server.
		/// </summary>
		/// <remarks>
		/// A key generated by <see cref="GetCacheKeyForUser"/> is passed to the <see cref="GetCacheKeyForImage(string,string)"/> method overload 
		/// along with the specified <paramref name="publicKey"/> to generate an image key that is returned to the caller.
		/// </remarks>
		/// <seealso cref="GeneratePublicCacheKey"/>
		/// <seealso cref="GetCacheKeyForUser"/>
		/// <param name="context">The <see cref="HttpContext"/> in which the key is generated.</param>
		/// <param name="publicKey">String that a client can use to request an image from the server.</param>
		/// <returns>String that uniquely identifies an image being persisted on the server.</returns>
		private static string GetCacheKeyForImage(HttpContext context, string publicKey)
		{
			return GetCacheKeyForImage(publicKey, GetCacheKeyForUser(context));
		}

		/// <summary>
		/// Generates a string that uniquely identifies an image being persisted on the server.
		/// </summary>
		/// <seealso cref="GeneratePublicCacheKey"/>
		/// <seealso cref="GetCacheKeyForUser"/>
		/// <param name="publicKey">String that a client can use to request an image from the server.</param>
		/// <param name="userKey">String that uniquely identifies the user of a request.</param>
		/// <returns>String that uniquely identifies an image being persisted on the server.</returns>
		private static string GetCacheKeyForImage(string publicKey, string userKey)
		{
			return publicKey + userKey;
		}

		/// <summary>
		/// Gets the <see cref="MemoryStream"/> of an image stored on the server for the specified <paramref name="context"/> and 
		/// <paramref name="publicKey"/>.
		/// </summary>
		/// <remarks>
		/// <para>
		/// The specified <paramref name="publicKey"/> is used to identify the image on the server.  Depending upon the value of the 
		/// <see cref="UserMode"/> property, the same user may have to make the original request to generate the key.
		/// </para>
		/// <para>
		/// The type of persistence that is used depends upon the value of the <see cref="PersistenceMode"/> property since the <see cref="getPersistedValue"/> 
		/// delegate is used to retrieve the stream.
		/// </para>
		/// <para>
		/// When <paramref name="keepAlive"/> is <see langword="true"/> the stream must not be disposed by the caller since it will remain on the server 
		/// with the same <paramref name="publicKey"/>.  In this case the sliding expiration will be reset when this method is called.
		/// </para>
		/// <para>
		/// If the image is removed from server persistence then the <paramref name="keepAlive"/> output argument is set to <see langword="false"/>.
		/// The caller should therefore dispose of the stream after it's used to free up memory.
		/// </para>
		/// <para>
		/// If the image has expired or if the specified <paramref name="publicKey"/> is invalid, <see langword="null"/> is returned.
		/// </para>
		/// </remarks>
		/// <seealso cref="GeneratePublicCacheKey"/>
		/// <seealso cref="GenerateAndCacheImage"/>
		/// <seealso cref="PersistenceMode"/>
		/// <seealso cref="UserMode"/>
		/// <exception cref="ArgumentException">The specified <paramref name="publicKey"/> is null or empty.</exception>
		/// <exception cref="SessionStateRequiredException">The specified <paramref name="context"/> does not have an enabled session and session state is required
		/// by AIP.</exception>
		/// <param name="context">The <see cref="HttpContext"/> in which the image is being retrieved.</param>
		/// <param name="publicKey">String that a client can use to request an image from the server.</param>
		/// <param name="keepAlive">Output argument that indicates whether the stream should be disposed by the caller.</param>
		/// <returns>The <see cref="MemoryStream"/> of an image stored on the server for the specified <paramref name="context"/> and <paramref name="publicKey"/> 
		/// or <see langword="null"/> if the image doesn't exist, possibly due to expiration.</returns>
		internal static MemoryStream GetCachedImageStream(HttpContext context, string publicKey, out bool keepAlive)
		{
			if (string.IsNullOrEmpty(publicKey))
				throw new ArgumentException(Resources.Errors.ImageKeyNotSpecified, "publicKey");

			string imageKey = GetCacheKeyForImage(context, publicKey);

			Pair data = (Pair) getPersistedValue(context, imageKey, false);

			if (data == null)
			{
				keepAlive = false;
				return null;
			}

			keepAlive = (bool) data.First;

			if (!keepAlive)
			{
				if (PersistenceMode == AutoInputProtectionPersistenceMode.Cache)
					context.Cache.Remove(imageKey);
				else
					context.Session.Remove(imageKey);
			}

			return (MemoryStream) data.Second;
		}

		/// <summary>
		/// Gets the text stored on the server, for the specified <paramref name="context"/> and <paramref name="publicKey"/> and immediately removes the text 
		/// from server persistence.
		/// </summary>
		/// <remarks>
		/// <para>
		/// The specified <paramref name="publicKey"/> is used to identify the text that corresponds with an AIP image for the same <paramref name="publicKey"/>
		/// and for the same <paramref name="context"/>.  Depending upon the value of the <see cref="UserMode"/> property, the same user may have to make the 
		/// original request to generate the key.
		/// </para>
		/// <para>
		/// The type of persistence that is used depends upon the value of the <see cref="PersistenceMode"/> property since the <see cref="getPersistedValue"/> 
		/// delegate is used to retrieve the text.
		/// </para>
		/// <para>
		/// After this method returns to the caller the same <paramref name="publicKey"/> cannot be used to retrieve the text again since it is no longer being
		/// persisted on the server.
		/// </para>
		/// <para>
		/// This method also forces the persisted image to expire immediately by calling <see cref="ExpireCachedImage"/>.  However, an image may only be requested
		/// once by default (i.e., an absolute expiration is used instead of a sliding expiration) and since the image must be requested before validation can occur
		/// this method is typically called after the image has already expired anyway.
		/// </para>
		/// </remarks>
		/// <exception cref="SessionStateRequiredException">The specified <paramref name="context"/> does not have an enabled session and session state is required
		/// by AIP.</exception>
		/// <seealso cref="GetCacheKeyForUser"/>
		/// <seealso cref="GeneratePublicCacheKey"/>
		/// <seealso cref="GetCacheKeyForImage(string,string)"/>
		/// <seealso cref="CacheText"/>
		/// <seealso cref="ExpireCachedImage"/>
		/// <seealso cref="PersistenceMode"/>
		/// <seealso cref="UserMode"/>
		/// <param name="context">The <see cref="HttpContext"/> in which the text is being retrieved.</param>
		/// <param name="publicKey">String that a client can use to request an image from the server.</param>
		/// <returns>The text that corresponds to an AIP image for the specified <paramref name="context"/> and <paramref name="publicKey"/> or <see langword="null"/>
		/// if the text doesn't exist, possibly due to expiration.</returns>
		internal static string GetCachedTextAndForceExpire(HttpContext context, string publicKey)
		{
			if (context == null)
				throw new ArgumentNullException("context");

			string userKey = GetCacheKeyForUser(context);
			string imageKey = GetCacheKeyForImage(publicKey, userKey);
			string textKey = userKey ?? "t_" + imageKey;

			try
			{
				string text = (string) getPersistedValue(context, textKey, true);

				System.Diagnostics.Debug.WriteLine(text + "; Key: " + textKey, "AIP Read Text");

				return text;
			}
			finally
			{
				ExpireCachedImage(context, imageKey);
			}
		}

		/// <summary>
		/// Removes the AIP image associated with the specified <paramref name="context"/> and <paramref name="internalKey"/> from server persistence.
		/// </summary>
		/// <remarks>
		/// <para>
		/// If the image stream exists it will be disposed.
		/// </para>
		/// <para>
		/// The type of persistence that is used depends upon the value of the <see cref="PersistenceMode"/> property since the <see cref="getPersistedValue"/> 
		/// delegate is used to retrieve the text.
		/// </para>
		/// </remarks>
		/// <exception cref="SessionStateRequiredException">The specified <paramref name="context"/> does not have an enabled session and session state is required
		/// by AIP.</exception>
		/// <seealso cref="GetCacheKeyForImage(string,string)"/>
		/// <seealso cref="PersistenceMode"/>
		/// <param name="context">The <see cref="HttpContext"/> in which the image will be removed.</param>
		/// <param name="internalKey">String that uniquely identifies the image on the server.</param>
		private static void ExpireCachedImage(HttpContext context, string internalKey)
		{
			if (!string.IsNullOrEmpty(internalKey))
			{
				Pair data = (Pair) getPersistedValue(context, internalKey, true);

				if (data != null)
				{
					MemoryStream stream = (MemoryStream) data.Second;
					stream.Dispose();
				}
			}
		}

		/// <summary>
		/// Gets a value with the specified <paramref name="internalKey"/> from the current <see cref="System.Web.SessionState.HttpSessionState"/>, for the specified 
		/// <paramref name="context"/>, and optionally removes it so that it cannot be used again.
		/// </summary>
		/// <seealso cref="PersistenceMode"/>
		/// <exception cref="SessionStateRequiredException">The specified <paramref name="context"/> does not have an enabled session.</exception>
		/// <param name="context">The <see cref="HttpContext"/> from which to retrieve the value.</param>
		/// <param name="internalKey">String that uniquely identifies the value to be retrieved from session state.</param>
		/// <param name="remove">Specifies whether the value should be removed from the session.</param>
		/// <returns>Value that corresponds with the specified <paramref name="internalKey"/> in the <see cref="System.Web.SessionState.HttpSessionState"/> of the 
		/// specified <paramref name="context"/>.</returns>
		private static object GetSessionValue(HttpContext context, string internalKey, bool remove)
		{
			if (context.Session == null)
				throw new SessionStateRequiredException();

			try
			{
				return context.Session[internalKey];
			}
			finally
			{
				if (remove)
					context.Session.Remove(internalKey);
			}
		}

		/// <summary>
		/// Gets a value with the specified <paramref name="internalKey"/> from the ASP.NET <see cref="System.Web.Caching.Cache">Cache</see> of the specified <paramref name="context"/>
		/// and optionally removes it so that it cannot be used again.
		/// </summary>
		/// <seealso cref="PersistenceMode"/>
		/// <param name="context">The <see cref="HttpContext"/> from which to retrieve the value.</param>
		/// <param name="internalKey">String that uniquely identifies the value to be retrieved from the cache.</param>
		/// <param name="remove">Specifies whether the value should be removed from the cache.</param>
		/// <returns>Value that corresponds with the specified <paramref name="internalKey"/> in the ASP.NET <see cref="System.Web.Caching.Cache">Cache</see> of the specified <paramref name="context"/>.</returns>
		private static object GetCacheValue(HttpContext context, string internalKey, bool remove)
		{
			try
			{
				return context.Cache[internalKey];
			}
			finally
			{
				if (remove)
					context.Cache.Remove(internalKey);
			}
		}
		#endregion
	}
}
